\title{Phase Lock Loop Components in myHDL} \author{Steven K Armour} \maketitle
This notebook is an exploration into the building and testing the Phase Lock Detector and the frequency divider components of an all Digital Phase Lock Loop. Here the Digitial Oscillator and the low pass filter are left for there own exploratory analysis to then allow the reader to design and implement there own PLL.
@misc{allen_2003, title={LECTURE 170 APPLICATIONS OF PLLS AND FREQUENCY DIVIDERS (PRESCALERS)}, author={Allen, Phillip E.}, year={2003} },
@phdthesis{gal_2012, title={Design of Fractional-N Phase Locked Loops For Frequency Synthesis From 30 To 40 GHz}, school={McGill University}, author={Gal, George}, year={2012} },
@misc{niknejad_2014, title={Phase Locked Loops (PLL) and Frequency Synthesis}, author={Niknejad, Ali M.}, year={2014} },
@book{razavi_2009, place={Upper Saddle River, NJ}, edition={1}, title={RF microelectronics}, publisher={Prentice Hall}, author={Razavi, Behzad}, year={2009}, pages={Chapter 8} }
@book{craninckx_steyaert_1998, place={New York}, title={Wireless CMOS frequency synthesizer design}, publisher={Springer}, author={Craninckx, J and Steyaert, M}, year={1998} pages={42-46} }
In [1]:
from myhdl import *
from myhdlpeek import Peeker
In [2]:
#helper functions to read in the .v and .vhd generated files into python
def VerilogTextReader(loc, printresult=True):
with open(f'{loc}.v', 'r') as vText:
VerilogText=vText.read()
if printresult:
print(f'***Verilog modual from {loc}.v***\n\n', VerilogText)
return VerilogText
def VHDLTextReader(loc, printresult=True):
with open(f'{loc}.vhd', 'r') as vText:
VerilogText=vText.read()
if printresult:
print(f'***VHDL modual from {loc}.vhd***\n\n', VerilogText)
return VerilogText
The phase lock loop (PLL) is one of the Six classical feedback topologies in electrical engineering. The others being the Voltage-Voltage (Series-Shunt), Voltage-Current(Shunt-Shunt), Current-Current (Shunt-Shunt), Current-Voltage (Series-Series), and the Autoregressive Moving Average (ARMA, aka IIR Filter). The PLL differs from the rest in that it is temporal compersion feedback system that only works for oscillatory information. Looking at the diagram below for the generic classical PLL we see that it is made of Five (six if a frequency divider is added to the reference clock) components. Two of which the Phase Detector and the output frequency divider that make up the feedback loop are somewhat unique to the PLL and are the primary concern of this notebook.
The other components that are part of the forward path are the filter that helps take out some of the "phase noise" error that creeps into the PLL and is typical of the Low Pass variety. The reference oscillator that since the PLL controls phase and phase is a measure of temporal displacement against a reference and the Controlled Oscillator. In brief, the controlled oscillator is made to accelerate its temporal progression (how fast it oscillates) relative to the reference oscillator the error provided by the feedback loop. Where the error is then given as
$$e(t)=K_{PD}(\Phi_{\text{ref}}(t)-\Phi_{\text{ocl}}(t)/N)$$Where
$K_{PD}$ is the transfer function of the Phase Detector that will be expanded upon shortly. the noting that the Transfer Function for the Voltage control oscillator is $K_{CO}$ and that of the filter is $H(s)$ the resulting open loop and closed loop gain can be found to be
$$A(s)=\dfrac{K_{PD} H(s) K_{CO}}{Ns}$$$$G(s)=\dfrac{K_{PD} H(s) K_{CO}}{s+K_{PD} H(s) \dfrac{K_{CO}}{N}}$$The phase detector (PD) is the only must-have component to a PLL that makes a PLL a PLL via calculating the phase offset error from a reference frequency source and the controlled frequency source via the negative feedback loop. It is the job of the PD to ensure that the waveform of the output is within some portion is in sync with the reference waveform thereby creating a phase lock. Where if the reface waveform and the controlled waveform are out of sync then the loop is said to be unlocked. While for analog PLL there are a variety of PDs which are mostly based on mixers (see Wiki Phase detector) for digital Phase Detectors there are basically only two kinds. The very primitive Negated XOR and the DFlipFlop Stat machine variety
In [3]:
@block
def NXORPD(clkREF, clkFB, LOCK):
"""
Negated XOR Phase Detector
I/O:
clkREF (bool; in): Ref clock
clkFB (bool; in): Compere clock
LOCK (bool; out): Negated XOR (LOCK) result
"""
@always_comb
def logic():
LOCK.next= not (clkREF^clkFB)
return instances()
In [4]:
#clear peeker and create test signals
Peeker.clear()
clkREF=Signal(bool(0)); Peeker(clkREF, 'clkREF')
clkFB=Signal(bool(0)); Peeker(clkFB, 'clkFB')
LOCK=Signal(bool(0)); Peeker(LOCK, 'LOCK')
#this clk is a Witness clock refrance
clk=Signal(bool(0)); Peeker(clk, 'clk')
#bind the signals to the DUT
DUT=NXORPD(clkREF, clkFB, LOCK)
def NXORPD_TB(RefDelay=2, FBDelay=4):
"""
Negated XOR Phase Detector Testbench
Args:
RefDelay (int; 2): refrance clock delay cyles compared to refrance
FBDelay (int; 4): feedback clock delay cyles compared to refrance
"""
#witness clock
@always(delay(1))
def clkGen():
clk.next=not clk
#refrance clock
@always(delay(RefDelay))
def RefClkGen():
clkREF.next=not clkREF
#feedback clock
@always(delay(FBDelay))
def FBClkGen():
clkFB.next=not clkFB
#run the simulation
@instance
def stimulus():
for i in range(100):
yield clk.posedge
raise StopSimulation()
return instances()
sim=Simulation(DUT, NXORPD_TB(), *Peeker.instances()).run()
In [5]:
Peeker.to_wavedrom(start_time=0, stop_time=20)
In [6]:
#pull the sim data into a PD dataframe
NXORPDRes=Peeker.to_dataframe()
#reorder the collums
NXORPDRes=NXORPDRes.reindex(columns=['clk', 'clkREF', 'clkFB', 'LOCK']);NXORPDRes
#show the top ten
NXORPDRes.head(10)
Out[6]:
In [7]:
#review what the Ref and FB clock values where when the PD was locked
NXORPDRes[NXORPDRes['clk']==1][NXORPDRes['LOCK']==1].head(10)
Out[7]:
In [8]:
#review what the Ref and FB clock values where when the PD was unlocked
NXORPDRes[NXORPDRes['clk']==1][NXORPDRes['LOCK']==0].head(10)
Out[8]:
In [9]:
DUT.convert()
VerilogTextReader('NXORPD');
The Sequential PD is the most common digital phase detector around and while there are variations of this architecture that can be found they all based on this simple but ingenious architecture. The architecture consists of two DFF where unlike in a typical state machine where a master clock control the flipping of the DFFs and the Data line into the DFFs is part of the State machine feedback loop. Here the DFFs are each on an independent clock where here the upper DFF is tied to the Reference Clock and the lower one is tied to the Feedback Clock. Therefore each one will output a high signal at a rate determined by the frequency of there respective clocks.
But that is not the end to the cleverness of this topology. The outputs of the DFFs are continuously compared by an AND gate such that only when the DFFs are in sync ($\omega_{\text{REF}}=\omega_{\text{FB}}$) will a very brief high spike will show up on both Next state lines of the DFF before the two DFFs are reset. For the other two conditions possible, only one of the two lines will have a high-value present. If $\omega_{\text{REF}}>\omega_{\text{FB}}$ then the lower output will be zero. And conversely if $\omega_{\text{REF}}<\omega_{\text{FB}}$ the upper output will be zero.
The above conditions as described by Razavi yield the following state machine for the Sequential Phase Detector
Note that the actual phase detection is the average of the two output lines. Wich not shown here, where one method to find the average is by scaling the boolean outputs to digital words and then pass the resultant words to an average and then to a low pass filter in order to implement the PLL.
In [10]:
@block
def SeqPD(clkREF, clkFB, UpOut, DownOut):
"""
Sequential DFF Phase Detector
I/O:
clkREF (bool; in): Ref clock to Upper DFF
clkFB (bool; in): Compare clock to Lower DFF
UpOut (bool; ouput): Upper DFF ouput
DownOut (bool; output): Lower DFF ouput
"""
#and clear internal feedback sig
clr = ResetSignal(0, active=0, async=True)
#upper DFF
@always(clkREF.posedge, clr.posedge)
def UpD():
if clr:
UpOut.next=0
else:
UpOut.next=1
#lower DFF
@always(clkFB.posedge, clr.posedge)
def DownD():
if clr:
DownOut.next=0
else:
DownOut.next=1
#and clear
@always_comb
def clrLogic():
clr.next= UpOut and DownOut
return instances()
In [11]:
#create the test signals
Peeker.clear()
clkREF=Signal(bool(0)); Peeker(clkREF, 'clkREF')
clkFB=Signal(bool(0)); Peeker(clkFB, 'clkFB')
UpOut=Signal(bool(0)); Peeker(UpOut, 'UpOut')
DownOut=Signal(bool(0)); Peeker(DownOut, 'DownOut')
#bind the test signals to the DUT
DUT=SeqPD(clkREF, clkFB, UpOut, DownOut)
def SeqPD_TB(RefDelay, FBDelay):
"""
Test bench for Sequantial Testbench
Args:
RefDelay (int; 2): refrance clock delay cyles
FBDelay (int; 4): feedback clock delay cyles
"""
#refrance clock
@always(delay(RefDelay))
def RefClkGen():
clkREF.next=not clkREF
#feedback clock
@always(delay(FBDelay))
def FBClkGen():
clkFB.next=not clkFB
#run the simulation
@instance
def stimulus():
for i in range(50):
yield clkREF.posedge
raise StopSimulation()
return instances()
In [12]:
#create the test signals
Peeker.clear()
clkREF=Signal(bool(0)); Peeker(clkREF, 'clkREF')
clkFB=Signal(bool(0)); Peeker(clkFB, 'clkFB')
UpOut=Signal(bool(0)); Peeker(UpOut, 'UpOut')
DownOut=Signal(bool(0)); Peeker(DownOut, 'DownOut')
#bind the test signals to the DUT
DUT=SeqPD(clkREF, clkFB, UpOut, DownOut)
sim=Simulation(DUT, SeqPD_TB(3, 2), *Peeker.instances()).run()
Peeker.to_wavedrom(start_time=0, stop_time=20)
In [13]:
#create the test signals
Peeker.clear()
clkREF=Signal(bool(0)); Peeker(clkREF, 'clkREF')
clkFB=Signal(bool(0)); Peeker(clkFB, 'clkFB')
UpOut=Signal(bool(0)); Peeker(UpOut, 'UpOut')
DownOut=Signal(bool(0)); Peeker(DownOut, 'DownOut')
#bind the test signals to the DUT
DUT=SeqPD(clkREF, clkFB, UpOut, DownOut)
sim=Simulation(DUT, SeqPD_TB(1, 1), *Peeker.instances()).run()
Peeker.to_wavedrom(start_time=0, stop_time=20)
In [14]:
#create the test signals
Peeker.clear()
clkREF=Signal(bool(0)); Peeker(clkREF, 'clkREF')
clkFB=Signal(bool(0)); Peeker(clkFB, 'clkFB')
UpOut=Signal(bool(0)); Peeker(UpOut, 'UpOut')
DownOut=Signal(bool(0)); Peeker(DownOut, 'DownOut')
#bind the test signals to the DUT
DUT=SeqPD(clkREF, clkFB, UpOut, DownOut)
sim=Simulation(DUT, SeqPD_TB(2, 3), *Peeker.instances()).run()
Peeker.to_wavedrom(start_time=0, stop_time=20)
In [15]:
DUT.convert()
VerilogTextReader('SeqPD');
Frequency Dividers (more properly called fractional frequency dividers) are and are not a type of counter. In a traditional counter the counter would be run off a master clock to increment the counter and when the specified count is reached an indication signal would be given while also resetting the counter. In a fractional frequency divider, we can increment a counter but the incrimination is run off the input clock which may or may not be the master clock and the output of the counter reaching its count would be a then the new divided output clock. In addition, we can create "fractional" dividers such as $2/3$ via cascading a $1/2$ and a $1/3$ and modulating between them from another clock source to control the modulation.
The reason that "fractional" is in quotes in regards to $2/3$ is that the frequency divider is not actually dividing by $2/3$ but is instead switching between a $1/2$ and a $1/3$ divider. Thus $2/3$ divider is a notational misnomer. But by cascading fixed and variable dividers with counters to control the modulation based on the clock being cascaded from large fractional division can be optioned
So as stated frequency dividers are and are not a counter through some of the more advanced programmable frequency dividers such as http://tremaineconsultinggroup.com/fractional-divider-in-verilog/ (source code: https://bitbucket.org/BrianTremaine/fractional_divide/src/a68c67979c80a453d4f7dfd82f5bf90d604393f1/hardware/frac_divider.v?at=master&fileviewer=file-view-default) ) are much more like counters then the primitive ones that will be discussed here
In [16]:
@block
def Div2FD(clkIN, clkOUT, rst):
"""
1/2 fractial freancy divider
I/O:
clkIN (input bool): input clock signal
clkOUT (ouput bool): 1/2 ouput clock signal
rst (input bool): reset signal
"""
W12=Signal(bool(0))
@always(clkIN.posedge)
def D1():
if rst:
W12.next=0
else:
W12.next=clkOUT
@always(clkIN.posedge)
def D2():
if rst:
clkOUT.next=0
else:
clkOUT.next= not W12
return instances()
In [17]:
Peeker.clear()
clkIN=Signal(bool(0)); Peeker(clkIN, 'clkIN')
clkOUT=Signal(bool(0)); Peeker(clkOUT, 'clkOUT')
rst=Signal(bool(0)); Peeker(rst, 'rst')
DUT=Div2FD(clkIN, clkOUT, rst)
def Div2FD_TB():
#input clock source
@always(delay(1))
def ClkGen():
clkIN.next=not clkIN
#run the simulation
@instance
def stimulus():
for i in range(21):
yield clkIN.posedge
raise StopSimulation()
return instances()
sim=Simulation(DUT, Div2FD_TB(), *Peeker.instances()).run()
In [18]:
Peeker.to_wavedrom(start_time=0, stop_time=21)
In [19]:
DUT.convert()
VerilogTextReader('Div2FD');
In [20]:
@block
def Div3FD(clkIN, clkOUT, rst):
"""
1/3 fractial freancy divider
I/O:
clkIN (input bool): input clock signal
clkOUT (ouput bool): 1/2 ouput clock signal
rst (input bool): reset signal
"""
W1A, WA2=[Signal(bool(0)) for _ in range(2)]
@always(clkIN.posedge)
def D1():
if rst:
W1A.next=0
else:
W1A.next=clkOUT
@always(clkIN.posedge)
def D2():
if rst:
clkOUT.next=0
else:
clkOUT.next= not WA2
@always_comb
def And():
WA2.next=W1A and clkOUT
return instances()
In [21]:
Peeker.clear()
clkIN=Signal(bool(0)); Peeker(clkIN, 'clkIN')
clkOUT=Signal(bool(0)); Peeker(clkOUT, 'clkOUT')
rst=Signal(bool(0)); Peeker(rst, 'rst')
DUT=Div3FD(clkIN, clkOUT, rst)
def Div3FD_TB():
#input clock source
@always(delay(1))
def ClkGen():
clkIN.next=not clkIN
#run the simulation
@instance
def stimulus():
for i in range(31):
yield clkIN.posedge
raise StopSimulation()
return instances()
sim=Simulation(DUT, Div3FD_TB(), *Peeker.instances()).run()
In [22]:
Peeker.to_wavedrom(start_time=0, stop_time=19)
In [23]:
DUT.convert()
VerilogTextReader('Div3FD');
In [24]:
@block
def Div23FD(clkIN, ModControl, clkOUT, rst):
"""
2/3 fractial freancy divider
I/O:
clkIN (input bool): input clock signal
ModControl (input bool): modulation switching signal
low is 1/3 high is 1/2
clkOUT (ouput bool): 1/2 ouput clock signal
rst (input bool): reset signal
"""
W1O, WOA, WA2=[Signal(bool(0)) for _ in range(3)]
@always(clkIN.posedge)
def D1():
if rst:
W1O.next=0
else:
W1O.next=clkOUT
@always(clkIN.posedge)
def D2():
if rst:
clkOUT.next=0
else:
clkOUT.next=not WA2
@always_comb
def OR():
WOA.next=W1O or ModControl
@always_comb
def AND():
WA2.next=clkOUT and WOA
return instances()
In [25]:
Peeker.clear()
clkIN=Signal(bool(0)); Peeker(clkIN, 'clkIN')
ModControl=Signal(bool(1)); Peeker(ModControl, 'ModControl')
clkOUT=Signal(bool(0)); Peeker(clkOUT, 'clkOUT')
rst=Signal(bool(0)); Peeker(rst, 'rst')
DUT=Div23FD(clkIN, ModControl, clkOUT, rst)
def Div23FD_TB():
#input clock source
@always(delay(1))
def ClkGen():
clkIN.next=not clkIN
#run the simulation
@instance
def stimulus():
for i in range(31):
yield clkIN.posedge
if i>4:
ModControl.next=0
raise StopSimulation()
return instances()
sim=Simulation(DUT, Div23FD_TB(), *Peeker.instances()).run()
In [26]:
Peeker.to_wavedrom(start_time=0, stop_time=8)
In [27]:
Peeker.to_wavedrom(start_time=10, stop_time=23)
In [28]:
Peeker.to_wavedrom(start_time=0, stop_time=23)
In [29]:
DUT.convert()
VerilogTextReader('Div23SFD');
In [30]:
@block
def Div45FD(clkIN, ModControl, clkOUT, rst):
"""
4/5 fractial freancy divider
I/O:
clkIN (input bool): input clock signal
ModControl (input bool): modulation switching signal
low is 1/4 high is 1/5
clkOUT (ouput bool): 1/2 ouput clock signal
rst (input bool): reset signal
"""
WNA11, W12, W2NA1, W2NA2, WNA23, W3NA1=[Signal(bool(0)) for _ in range(6)]
@always_comb
def NAND1():
WNA11.next=not(W2NA1 and W3NA1 )
@always(clkIN.posedge)
def D1():
if rst:
W12.next=0
clkOUT.next=0
else:
W12.next=WNA11
clkOUT.next= WNA11
@always(clkIN.posedge)
def D2():
if rst:
W2NA1.next=0
W2NA2.next=0
else:
W2NA1.next=W12
W2NA2.next=not W12
@always_comb
def NAND2():
WNA23.next=not(W2NA2 and ModControl)
@always(clkIN.posedge)
def D3():
if rst:
W3NA1.next=0
else:
W3NA1.next=WNA23
return instances()
In [31]:
Peeker.clear()
clkIN=Signal(bool(0)); Peeker(clkIN, 'clkIN')
ModControl=Signal(bool(0)); Peeker(ModControl, 'ModControl')
clkOUT=Signal(bool(0)); Peeker(clkOUT, 'clkOUT')
rst=Signal(bool(0)); Peeker(rst, 'rst')
DUT=Div45FD(clkIN, ModControl, clkOUT, rst)
def Div45FD_TB():
#input clock source
@always(delay(1))
def ClkGen():
clkIN.next=not clkIN
#run the simulation
@instance
def stimulus():
for i in range(40):
yield clkIN.posedge
if i>12:
ModControl.next=1
raise StopSimulation()
return instances()
sim=Simulation(DUT, Div45FD_TB(), *Peeker.instances()).run()
In [32]:
Peeker.to_wavedrom(start_time=0, stop_time=20)
In [33]:
Peeker.to_wavedrom(start_time=20, stop_time=40)
In [34]:
Peeker.to_wavedrom(start_time=0, stop_time=40)
In [35]:
DUT.convert()
VerilogTextReader('Div45FD');
In [36]:
@block
def Div6FD(clkIN, clkOUT, rst):
"""
1/6 fractial freancy divider via cascaded 2 and 3 dividers
using two `Div23SFD`
I/O:
clkIN (input bool): input clock signal
clkOUT (ouput bool): 1/6 ouput clock signal
rst (input bool): reset signal
"""
clkMid=Signal(bool(0))
MC2=Signal(bool(1)); MC3=Signal(bool(0))
D2=Div23FD(clkIN, MC2, clkMid, rst)
D3=Div23FD(clkMid, MC3, clkOUT, rst)
return instances()
In [37]:
Peeker.clear()
clkIN=Signal(bool(0)); Peeker(clkIN, 'clkIN')
clkOUT=Signal(bool(0)); Peeker(clkOUT, 'clkOUT')
rst=Signal(bool(0)); Peeker(rst, 'rst')
DUT=Div6FD(clkIN, clkOUT, rst)
def Div6FD_TB():
#input clock source
@always(delay(1))
def ClkGen():
clkIN.next=not clkIN
#run the simulation
@instance
def stimulus():
for i in range(31):
yield clkIN.posedge
raise StopSimulation()
return instances()
sim=Simulation(DUT, Div6FD_TB(), *Peeker.instances()).run()
In [38]:
Peeker.to_wavedrom(start_time=0, stop_time=21)
In [39]:
DUT.convert()
VerilogTextReader('Div6FD');